package game.dungeon;

import game.dungeon.position.Direction;
import game.dungeon.position.Position;
import game.square.SquareImpl;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Raw;

/**
 * A class of composite dungeons as a special case of a dungeon.
 * 
 * @invar 	.. 
 * 		  | hasProperSubDungeons()
 * 
 * @version 1.0
 * @author Chris Desomer, Geert Heyman
 */
public class CompositeDungeon<S extends SquareImpl> extends Dungeon<S> {
	
	/**
	 * Initialize this new composite dungeon.
	 * 
	 * @param 	boundaryPosition
	 * 			The boundaryPosition for this composite dungeon.
	 * @effect	..
	 * 		  | super(boundaryPosition)
	 * @post	..
	 * 		  | getDirectSubDungeons() != null && getDirectSubDungeons().isEmpty()
	 */
	public CompositeDungeon(Position boundaryPosition) {
		super(boundaryPosition);
		directSubDungeons = new HashMap<Position,Dungeon<S>>();
	}
	
	/**
	 * Get a copy of the direct sub dungeons of this composite dungeon.
	 */
	@Basic
	public Map<Position,Dungeon<S>> getDirectSubDungeons(){
		return new HashMap<Position,Dungeon<S>>(directSubDungeons);
	}
	
	/**
	 * Check if this composite dungeon can have the given dungeon as one of 
	 * its sub dungeons, taking in account the given position for the origin 
	 * of the sub dungeon.
	 * 
	 * @param	dungeon
	 * 			The sub dungeon to check.
	 * @param	positionOrigin
	 * 			The position of the origin of the given sub dungeon relative 
	 * 			to this composite dungeon.
	 * @return	..
	 * 		  | result == positionOrigin.add(dungeon.getBoundaryPosition())
	 * 		  |		.isWithinBoundaryOf(getBoundaryPosition())
	 */
	@Raw
	public boolean canHaveAsSubDungeonAtPosition(Dungeon<? extends S> dungeon, Position positionOrigin){
		Position boundaryPositionGiven = positionOrigin.add(dungeon.getBoundaryPosition());
		if (!boundaryPositionGiven.isWithinBoundaryOf(getBoundaryPosition()))
			return false;
		return true;
	}
	
	/**
	 * Method to add a sub dungeon to this composite dungeon.
	 * 
	 * @param	subDungeon
	 * 			The sub dungeon to at to this composite dungeon.
	 * @param	positionOrigin
	 * 			The position of the origin of the sub dungeon to add
	 * 			in reference to the origin of this composite dungeon.
	 * @post	..
	 * 		  | getDirectSubDungeons().get(positionOrigin) == subDungeon
	 * @throws	IllegalArgumentException
	 * 			This composite dungeon can't have the given sub dungeon as
	 * 			one of its sub dungeons.
	 * @throws	IllegalArgumentException
	 * 			At least one position of the  given sub dungeon, located at the 
	 * 			given origin position, overlaps with one of the direct sub dungeons
	 * 			of this composite dungeon.  
	 */
	public void addSubDungeon (Dungeon<S> subDungeon, Position positionOrigin) throws IllegalArgumentException{
		if (!canHaveAsSubDungeonAtPosition(subDungeon, positionOrigin))
			throw new IllegalArgumentException();
		if (!subDungeon.isDisjunctWithDungeonsOfCompositeDungeon(this, positionOrigin))
			throw new IllegalArgumentException();
		directSubDungeons.put(positionOrigin, subDungeon);
		subDungeon.setEnclosingDungeon(this);
	}
	
	/**
	 * Check if this composite dungeon has proper sub dungeons.
	 * 
	 * @return	..
	 * 		  | For each originPosition in getDirectSubDungeons().keySet()
	 * 		  |		if (!canHaveAsSubDungeonAtPosition(getDirectSubDungeons().get(originPosition), originPosition))
	 * 		  |			then result == false
	 * @result ..
	 * 		  | For each originPosition in getDirectSubDungeons().keySet()
	 * 		  |		if (!getDirectSubDungeons().get(originPosition).getEnclosingDungeon() != this )
	 * 		  |			then result == false
	 * @result	..
	 * 		  | For each originPosition in getDirectSubDungeons().keySet()
	 * 		  |		if (!getDirectSubDungeons().get(originPosition).
	 * 		  |				isDisjunctWithDungeonsOfCompositeDungeon(this, originPosition)
	 * 		  |			result == false
	 * @result	..
	 * 		  | else result == true
	 */
	@Raw
	public boolean hasProperSubDungeons(){
		for (Position origin : directSubDungeons.keySet()){
			Dungeon<S> subDungeon = directSubDungeons.get(origin);
			if (!canHaveAsSubDungeonAtPosition(subDungeon, origin))
				return false;
			if (subDungeon.getEnclosingDungeon() != this)
				return false;
			if (!subDungeon.isDisjunctWithDungeonsOfCompositeDungeon(this, origin))
				return false;		
		} 
		return true;		
	}
	
	/**
	 * Variable referencing the set of direct sub dungeons of this composite dungeon.
	 */
	private Map<Position,Dungeon<S>> directSubDungeons;
	
	/**
	 * Add a square to this dungeon at the given position.
	 * 
	 * @param 	square
	 * 			The square to add.
	 * @param 	position
	 * 			The position where the square should be added.
	 * @pre 	...
	 * 		  | canHaveAsSquareAtPosition(square,position)
	 * @pre		...	
	 * 		  |	square.getNbOfDungeons() == 0
	 * @pre		The given square isn't slippery or the percentage of slippery squares
	 * 			if the given square is added isn't higher than 20%
	 * @pre		..
	 * 		  | position != null
	 * @pre		..
	 * 		  | !getSingularDungeons().isEmpty()
	 * @post	...
	 * 		  |	new.getSquares().contains(square)
	 * @effect	...
	 * 		  |	square.incrementNbOfDungeons()
	 * @post	...
	 * 		  | For each direction in Direction
	 * 		  |		if (getSquares().get(position.getPositionInDirection(direction)) != null)
	 * 		  |			new.getSquares().get(position.getPositionInDirection(direction)).getBorderInDirection(direction
	 * 		  |				.getOppositeDirection()) == square.getBorderInDirection(direction)
	 */
	@Override
	public void addSquareAtPosition(S square,Position position) {
		super.addSquareAtPosition(square, position);
		assert (position != null);
		assert (!getSingularDungeons().isEmpty());
		Position subDungeonOrigin = getSubDungeonOriginEnclosingPosition(position);
		Dungeon<S> subDungeon = directSubDungeons.get(subDungeonOrigin);	// check that the subdungeon type of squares for the subdungeon is of type T or a superclass of T
		if (isBorderPositionOfSubDungeon(position, subDungeonOrigin))
			addToNeighborSquares(square,position);
		subDungeon.addSquareAtPosition(square, position.substract(subDungeonOrigin));
	}
	
	/*
	 * Auxiliary method to add the given square as a neighbor to the squares
	 * of this dungeon who have a position that borders the given position.
	 */
	private void addToNeighborSquares(S square,Position position) {
		for (Direction direction : Direction.values()){
			S neighborSquare = getSquares().get(position.getPositionInDirection(direction));
			if (neighborSquare != null)
				square.addNeighborInDirection(neighborSquare, direction);
		}
	}
	
	/*
	 * An auxiliary method to check of the given position is a border position of the given subdungeon.
	 */
	private boolean isBorderPositionOfSubDungeon(Position position,
			Position subDungeonOrigin) {
		Dungeon<S> subDungeon = directSubDungeons.get(subDungeonOrigin);
		if (position.substract(subDungeonOrigin).hasEqualCoordinateAs(subDungeon.getBoundaryPosition()))
			return true;
		return position.substract(subDungeonOrigin).hasEqualCoordinateAs(subDungeonOrigin);
	}

	/**
	 * Remove the given square from the squares of this composite dungeon.
	 * 
	 * @param 	square
	 * 			The square to remove.
	 * @pre		..
	 * 		  | square != null
	 * @pre		..
	 * 		  |	getSquares().containsValue(square)
	 * @post	..
	 * 		  | !getSquares().contains(square)
	 * @post	If the given square is on of the squares in this composite dungeon
	 * 			the given square is terminated.
	 */
	@Override
	public void removeSquare(SquareImpl square){
		super.removeSquare(square);
		for (SingularDungeon<? extends S> singularDungeon : getSingularDungeons())
			if (singularDungeon.getSquares().containsValue(square))
				singularDungeon.removeSquare(square);
	}

	/*
	 * An auxiliary method to get the origin enclosing position of the subdungeon
	 */
	private Position getSubDungeonOriginEnclosingPosition(Position position) {
		Position originEnlosingPosition = null;
		for (Position subDungeonOrigin : directSubDungeons.keySet()){
			Position positionInSubDungeon = position.substract(subDungeonOrigin);
			Position boundaryPositionSubDungeon = directSubDungeons.get(subDungeonOrigin).getBoundaryPosition();
			if (positionInSubDungeon.isWithinBoundaryOf(boundaryPositionSubDungeon))
				originEnlosingPosition = subDungeonOrigin;	
		}
		return originEnlosingPosition ;
	}

	/**
	 * Check if this composite dungeon contains the given position.
	 * 
	 * @param	position
	 * 			The position to check.
	 * @return	..
	 * 		  |	if (!position.isWithinBoundaryOf(getBoundaryPosition()))
	 * 		  |		then result == false
	 * @result	..
	 * 		  | For each originSubDungeon in getDirectSubDungeons().keySet()
	 * 		  |		if (getDirectSubDungeons().get(originSubDungeon).
	 * 		  |				containsPosition(position.substract(originSubDungeon))
	 * 		  |			result == true
	 * @result	..
	 * 		  | else then result == false
	 */
	@Override
	public boolean containsPosition(Position position) {
		if (position.isWithinBoundaryOf(getBoundaryPosition()))
			return true;
		for (Position originSubDungeon : directSubDungeons.keySet()){
			Dungeon<? extends S> subDungeon = directSubDungeons.get(originSubDungeon);
			// substract to origin of the sub dungeon from to position, to get 
			//the position in the axes of the subdungeon.
			if (subDungeon.containsPosition(position.substract(originSubDungeon)))	
				return true;
		}
		return false;
	}

	/**
	 * Return a map with all the squares of this composite dungeon.
	 * 
	 * @return	A map with all the squares contained by the singular
	 * 			dungeons contained by this composite dungeon, the key
	 * 			of the map is the position of the square expressed in 
	 * 			this composite dungeon.
	 */
	@Override
	public Map<Position, S> getSquares() {
		HashMap<Position, S> squares = new HashMap<Position, S>();
		for (Position originSubDungeon : getDirectSubDungeons().keySet()){
			Map<Position, ? extends S> newSquares = directSubDungeons.get(
					originSubDungeon).getSquares();
			
			squares.putAll(getAdjustedPositions(newSquares,originSubDungeon));
		}
		return squares;
	}
	
	/*
	 * An auxiliary method to calculate the adjusted positions of this dungeon.
	 */
	private Map<Position,? extends S> getAdjustedPositions(Map<Position,? extends S> newSquares,
			Position originSubDungeon) {
		Map<Position, S> adjustedSquares = new HashMap<Position,S>();
		for(Position originalPosition : newSquares.keySet())
			adjustedSquares.put(originalPosition.add(originSubDungeon),
					newSquares.get(originalPosition));
		return adjustedSquares;
	}

	/**
	 * Get a set of the singular dungeons enclosed by this composite dungeon.
	 * 
	 * @return 	A set of all the singular dungeons that are direct sub dungeons
	 * 			of this square or are singular dungeons of one of the direct 
	 * 			sub dungeons of this composite dungeon.
	 */
	@Override
	public Set<SingularDungeon<? extends S>> getSingularDungeons(){
		Set<SingularDungeon<? extends S>> singularDungeons = new HashSet<SingularDungeon<? extends S>>();
		for (Dungeon<? extends S> subDungeon : getDirectSubDungeons().values()){
			if (subDungeon instanceof SingularDungeon<?>)	// '?' is no problem here because it is certain that subDungeon has S as generic type
				singularDungeons.add((SingularDungeon<? extends S>)subDungeon);
			else if (subDungeon instanceof CompositeDungeon<?>)
				singularDungeons.addAll(((CompositeDungeon<? extends S>)subDungeon).getSingularDungeons());
		}
		return singularDungeons;
	}
}